Table of Contents

개요

  • 대부분의 문제는 긴 메소드에서 기인한다.
    • 긴 메소드는 많은 정보를 가지고 있고
    • 복잡한 로직에 의해 많은 정보가 묻혀버리기 쉽다.
  • Extract Method
    • 코드 덩어리를 별도의 메소드로 뽑아내는 것.
  • Inline Method
    • Extract Method와 반대 개념
    • 메소드 호출 부분을 해당 메소드의 몸체로 바꾸는 것.
    • 쪼개놓은 메소드가 제 구실을 못할 때 혹은 재 구성 해야 할 필요가 있을 때
  • Replace Temp With Query
    • 모든 임시변수를 제거하기 위해 사용
  • Split Temporary Variable
    • 하나의 임시변수가 여러 목적으로 사용된다면 임시변수를 필요한 만큼 선언해서 쓴다.
  • Replace Method with Method Object
    • 임시변수가 너무 꼬여서 제거하기 힘들 경우 새로운 클래스를 만들어서 분해한다.
  • Remove Assignment to Parameters
    • 파라미터에 값을 대입하고 있다면 임시변수를 사용한다.
  • Substitute Algorithm
    • 코드를 좀더 명확하게 하기 위해서 알고리즘을 새로 바꾼다.

Extract Method

그룹으로 묶을 수 있는 코드 조각이 있으면 별도의 메소드로 뽑아낸다.
코드의 목적이 잘 들어나는 메소드 이름을 짓는다.

수정 전수정 후
{code}
void printOwing(double amount) {
printBanner();

//상세 정보 표시
System.out.println("name: " + _name);
System.out.println("amount: " + amount);
}

 | 

void printOwing(double amount) {
printBanner();
printDetails(amount);
}

void printDetails(double amount) {
System.out.println("name: " + _name);
System.out.println("amount: " + amount);
}

 |

h3. 동기

지나치게 긴 메소드를 보거나, 주석이 필요한 코드를 보면 그 부분을 하나의 메소드로 뽑아낸다.

h3. 절차

* 메소드를 만들고 의도가 잘 드러나게 이름을 정한다.(어떻게가 아니고 *무엇을* 하는 지 알 수 있게 이름을 정한다)
* 뽑아내고자 하는 부분의 코드를 복사해서 새 메소드로 옮긴다.
* 원래 메소드에서 사용되고 있는 지역변수가 뽑아낸 코드에 있는 지 확인한다.
있다면 새로 만든 메소드의 임시변수로 선언한다.
* 뽑아낸 코드에서 지역변수의 값이 수정되는 지 본다.
** 수정된 결과를 관련된 변수에 대입할 수 없거나 수정되는 지역변수가 2개 이상이면 [#Split Temporary Variable]을 사용한 다음 다시 시도
** 임시변수는 [#Replace Temp with Query]로 제거 가능
* 뽑아낸 코드에서 읽기만 하는 변수는 새 메소드의 파라미터로 넘긴다.
* 원래 메소드에서 뽑아낸 코드 부분은 새 메소드를 호출하도록 바꾼다.
* 테스트 한다.

h3. 예제: 지역 변수가 없는 경우

|| 수정 전 || 수정 후 ||
| 

void printOwing(double amount) {
Enumeration e = _orders.elements();
double outstanding = 0.0;

// 배너 표시
System.out.println("*********************************");
System.out.println("******" + Customer Owes + "******");
System.out.println("*********************************");

while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}

//상세 정보 표시
System.out.println("name: " + _name);
System.out.println("amount: " + outstanding);
}

 | 

void printOwing(double amount) {
Enumeration e = _orders.elements();
double outstanding = 0.0;

printBanner();

while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}

//상세 정보 표시
System.out.println("name: " + _name);
System.out.println("amount: " + outstanding);
}

void printBanner() {
// 배너 표시
System.out.println("*********************************");
System.out.println("******" + Customer Owes + "******");
System.out.println("*********************************");
}

 |

h3. 예제: 지역 변수가 포함되어 있는 경우

* 값이 변하지 않고 읽히기만 하는 경우, 변수를 파라미터로 넘긴다.
* 객체를 파라미터로 넘길 수 있다.
|| 수정 전 || 수정 후 ||
| 

void printOwing(double amount) {
Enumeration e = _orders.elements();
double outstanding = 0.0;

printBanner();

while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}

//상세 정보 표시
System.out.println("name: " + _name);
System.out.println("amount: " + outstanding);
}

 | 

void printOwing(double amount) {
Enumeration e = _orders.elements();
double outstanding = 0.0;

printBanner();

while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}

printDetails(outstanding);
}

void printDetails(double outstanding) {
System.out.println("name: " + _name);
System.out.println("amount: " + outstanding);
}

 |

h3. 예제: 지역 변수에 다른 값을 여러 번 대입하는 경우

* [#Remove Assignments to Parameters]를 적용
* 임시변수에 값을 대입하는 경우
** 임시변수가 뽑아낸 코드 안에서 사용되는 경우
** 임시변수가 뽑아낸 코드 외부에서도 사용되는 경우
*** 뽑아낸 코드 이후에서 사용된다면, 뽑아낸 코드에서 임시변수의 바뀐 값을 리턴하도록 수정한다.

|| 수정 전 || 수정 후 || 최종 ||
| 

void printOwing(double amount) {
Enumeration e = _orders.elements();
double outstanding = 0.0;

printBanner();

while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}

printDetails(outstanding);
}

 | 

void printOwing(double amount) {
printBanner();
double outstanding += getOutstanding();
printDetails(outstanding);
}

void getOutstanding() {
Enumeration e = _orders.elements();
double outstanding = 0.0;

printBanner();

while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}

return outstanding;
}

 | 

void printOwing(double amount) {
printBanner();
double outstanding += getOutstanding();
printDetails(outstanding);
}

void getOutstanding() {
Enumeration e = _orders.elements();
double result = 0.0;

printBanner();

while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
result += each.getAmount();
}

return result;
}

 |

h2. Inline Method

호출하는 곳에 메소드의 몸체를 넣고 메소드를 삭제하라.
|| 수정 전 || 수정 후 ||
| 

int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}

boolean moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}

 | 

int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}

 |

h3. 동기
짧은 메소드를 다시 합쳐 큰 메소드로 만든다. 메소드 몸체가 이름만큼이나 명확하고 읽기 쉬운 코드일 때 합친다.

h3. 절차
* 메소드가 다형성을 가지고 있지 않은 지 확인한다.
** Subclass에서 override 하고 있는 메소드에는 적용하지 않는다.
* 메소드를 호출하는 부분을 모두 찾는다.
* 메소드 호출을 몸체로 바꾼다.
* 테스트 한다.
* 메소드 정의를 제거한다.

h2. Inline Temp
간단한 수식의 결과값을 가지는 임시변수를 모두 원래의 수식으로 바꿔라

|| 수정 전 || 수정 후 ||
| 

double basePrice = anOrder.basePrice();
return (basePrice > 1000);

 | 

return (anOrder.basePrice() > 1000);

 |

h3. 동기
[#Replace Temp with Query]의 한 부분으로 사용된다.

h3. 절차
* 임시변수를 final로 선언한 다음 컴파일 한다.
** 이것은 임시변수에 값이 단 한번만 대입되는 지 확인하기 위함이다.
* 임시변수를 참조하고 있는 곳을 모두 찾아서 assignment의 우변에 있는 수식으로 바꾼다.
* 테스트 한다.
* 임시변수의 선언과 대입문(assignment)를 제거한다.
* 다시 테스트 한다.

h2. Replace Temp With Query
어떤 수식의 결과값을 저장하기 위해서 임시변수를 사용하고 있다면 
이 수식을 뽑아서 새로운 메소드로 만들고 임시변수를 모두 찾아서 메소드 호출로 바꾼다.
새로 만든 메소드는 다른 메소드에서도 사용될 수 있다.
|| 수정 전 || 수정 후 ||
| 

double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;

 | 

if (basePrice() > 1000)
return basePrice() * 0.95;
else
return basePrice() * 0.98;
...
double basePrice() {
return _quantity * _itemPrice;
}

 |

h3. 동기
* 임시로 사용되고 특정 부분에만 의미를 가지므로 문제다.
* 메소드의 컨텍스트 안에서만 볼 수 있고, 메소드가 길어지는 경향이 있다.
* 지역변수는 메소드의 추출을 어렵게 하기 때문에 가능한 질의 메소드(Query Method)로 바꾼다.

h3. 절차
* 임시변수에 값이 한번만 대입되는 지 확인
** 여러 번 대입되는 경우에는 [#Split Temporary Variable]을 먼저 적용
* 임시변수를 final로 선언
* 대입문의 우변을 메소드로 추출
** 처음에는 메소드를 private으로 선언. 나중에 다른 곳에서도 사용하는 게 좋으면 그때 접근 권한을 수정한다.
** 추출한 메소드가 부작용이 없는 지 확인
* 테스트 한다.
* [#Inline Temp]를 적용

h3. 예제
p.149 ~ 150 참조

h2. Introduce Explaining Variable
복잡한 수식이 있는 경우, 수식의 결과나 수식의 일부에 자신의 목적을 잘 설명하는 이름으로 된 임시변수를 사용하라.
|| 수정 전 || 수정 후 ||
| 

if ( (platform.toUpperCase().indexOf("MAC") > -1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
wasInitialized() && resize > 0 ) {
// 작업
}

 | 

final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;

if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// 작업
}

 |

h3. 동기
* 조건문에서 각각의 조건의 뜻을 설명하는 이름의 변수로 만들어 사용할 때 유용하다.
* 긴 알고리즘에서 각 단계의 계산 결과를 잘 지어진 이름의 임시변수로 설명할 수 있다.

h3. 절차
* final 변수를 선언하고 복잡한 수식의 일부를 이 변수에 대입한다.
* 복잡한 수식을 임시변수로 바꾼다.
* 컴파일과 테스트한다.
* 다른 수식이 있다면 위의 절차를 반복한다.

h3. 예제
p.152 ~ 154참조 : [#Extract Method]를 적용했을 때와 비교
저자는 이 방법을 선호함. 왜냐하면,
이 메소드들을 객체의 다른 부분에서도 쓸 수 있기 때문이다.

그럼 언제 Introduce Explaining Variable을 쓰느냐하면 지역변수가 아주 많아서 손 쓰기 힘들 때이다.

h2. Split Temporary Variable
임시변수에 값을 여러 번 대입하는 경우, 각각의 대입에 대해서 따로따로 임시변수를 만들어라.
단, *loop* 안의 변수(for, while)나 *collecting temporary valuable*(메소드 실행동안 모이는 값을 담은 변수)는 제외!!

|| 수정 전 || 수정 후 ||
| 

double temp = 2 * (_height + _width);
System.out.println (temp);
temp = _height * _width;
System.out.println (temp);

 | 

final double perimeter = 2 * (_height + _width);
System.out.println (perimeter);
final double area = _height * _width;
System.out.println (area);

 |

h3. 동기
하나의 임시변수를 여러 용도로 사용하면 혼란을 초래할 수 있다.

h3. 절차
* 임시변수 선언부부과 임시변수에 값이 처음 대입된 곳에서 변수명을 바꾼다.
* 새로 만든 임시변수를 final로 선언한다.
* 두 번째로 대입하는 곳의 직전까지 새로 만든 임시변수명으로 바꾼다.
* 임시변수에 두 번째로 대입하는 곳에서 새로운 임시변수를 선언한다.
* 컴파일과 테스트한다.
* 각 단계를 반복한다.

h3. 예제
p.156 ~ 158 참조

h2. Remove Assignments to Parameters
파라미터에 값을 할당하는 코드가 있으면 임시변수를 사용하라.
|| 수정 전 || 수정 후 ||
| 

int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;

 | 

int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;

 |

h3. 동기
혼란스럽기 때문이다.
물론, 자바에서는 pass by value로만 파라미터가 전달되기 때문에 파라미터에 변경을 가하더라도
호출쪽 값이 바뀌거나 하지는 않는다.
어쨌든 여전히 혼란스러울 수 있다.


void aMethod(Object foo) {
foo.modifiInSomeWay(); //OK
foo = anotherObject; //문제가 됨
}



h3. 절차
* 파라미터를 위한 임시변수를 만든다.
* 파라미터에 값을 할당한 코드 이후의 파라미터를 모두 임시변수로 바꾼다.
* 파라미터에 대입하는 값을 임시변수에 대입하도록 바꾼다.
* 컴파일과 테스트한다.

h3. 예제
p.160 ~ 161 참조

h3. 자바에서 값에 의한 전달(Pass by value)
자바는 엄격하게, 모든 경우에 값에 의한 전달을 사용한다.

package com.oracleclub.refactoring.ch6;

public class Param {

/**

  • @param args
    */
    public static void main(String[] args) {
    int x = 5;
    triple✘;
    System.out.println("X after triple: " + x);
    }

private static void triple(int arg) {
arg *= 3;
System.out.println("arg in triple: " + arg);
}
}



결과
arg in triple: 15
X after triple: 5




package com.oracleclub.refactoring.ch6;

import java.util.Date;

public class ParamObject {

/**

  • @param args
    */
    public static void main(String[] args) {
    Date d1 = new Date("1 Apr 98");
    nextDateUpdate(d1);
    System.out.println("d1 after next: " + d1);

Date d2 = new Date("1 Apr 98");
nextDateReplace(d2);
System.out.println("d2 after next: " + d2);
}

private static void nextDateUpdate(Date d1) {
d1.setDate(d1.getDate() + 1);
System.out.println("d1 in next day: " + d1);
}

private static void nextDateReplace(Date d2) {
d2 = new Date(d2.getYear(), d2.getMonth(), d2.getDate() + 1);
System.out.println("d2 in next day: " + d2);
}
}




결과
d1 in next day: Thu Apr 02 00:00:00 KST 1998
d1 after next: Thu Apr 02 00:00:00 KST 1998
d2 in next day: Thu Apr 02 00:00:00 KST 1998
d2 after next: Wed Apr 01 00:00:00 KST 1998



h2. Replace Method with Method Object
지역변수때문에 [#Extract Method]를 적용할 수 없는 경우, 그 메소드를 객체로 바꿔서 모든 지역변수가 그 객체의 필드가 되도록 한다.

class Order...
double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation;
...
}


!arrow.gif|hspace=180!
!repMethodWithObj.gif!

여기서는 price() 메소드를 PriceCalculator 객체로 바꿨다.

h3. 동기
거대한 메소드는 작은 부분을 뽑아냄으로써 코드를 더 이해하기 쉽게 만든다.
지역변수가 많아 메소드 분해가 어려울 때 [#Replace Temp with Query]를 쓸 수 있지만 이것으로도 해결되지 않을 때 
이 방법을 쓴다.

h3. 절차
* 메소드 이름을 따서 새로운 클래스를 만든다.
* 새로운 클래스에 원래 메소드에 있던 객체(Source Object)를 보관하기 위한 final 필드를 하나 만들고,
메소드에서 사용되는 임시변수와 파라미터를 위한 필드를 만든다.
* 새로운 클래스에 Source Object와 파라미터를 취하는 생성자를 만든다.
* 새로운 클래스에 compute 라는 이름의 메소드를 만든다.
* 원래의 메소드를 compute 메소드에 복사한다.
* 원래의 객체(Source Object)에 있는 메소드를 사용하는 경우는 소스 객체 필드를 사용하도록 바꾼다.
* 컴파일 한다.
* 새로운 클래스의 객체를 만들고 원래의 메소드를 새로 만든 객체의 compute 메소드를 호출하도록 바꾼다.

h3. 예제
p.165 ~ 166 참조

h2. Substitute Algorithm
알고리즘을 보다 명확한 것으로 바꾸고 싶을 때는 메소드의 몸체를 새로운 알고리즘으로 바꾼다.
|| 수정 전 || 수정 후 ||
| 

String foundPerson(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals ("Don")){
return "Don";
}
if (people[i].equals ("John")){
return "John";
}
if (people[i].equals ("Kent")){
return "Kent";
}
}
return "";
}

 | 

String foundPerson(String[] people){
List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i=0; i<people.length; i++)
if (candidates.contains(people[i]))
return people[i];
return "";
}

 |

h3. 동기
복잡한 것을 간단하고 명확한 알고리즘으로 바꾼다. 알고리즘이 간단해야 치환도 쉽다.

h3. 절차
* 대체 알고리즘을 준비한다. 적용하여 컴파일한다.
* 알고리즘을 테스트한다. 결과가 같으면 작업은 완료된 것이다.
* 결과가 같지 않으면 예전의 알고리즘을 사용하여 디버깅한다.
** 예전 알고리즘과 새 알고리즘에 대해 각각의 테스트 케이스를 실행시키고 두 결과를 본다.

h2. 문서에 대하여

* 이 문서의 내용은 [Refactoring\- 나쁜 디자인의 코드를 좋은 디자인으로 바꾸는 방법|http://kangcom.com/common/bookinfo/bookinfo.asp?sku=200204020003] 교재를 스터디 하면서 정리한 내용 입니다.
* 최초작성자 : [유진우]
* 최초작성일 : 2007년 12월 13일
* 이 문서는 [오라클클럽|http://www.gurubee.net] [자바 웹개발자 스터디|2007년 하반기 - 제2차 자바 웹개발 스터디] 모임에서 작성하였습니다.
* 이 문서를 다른 블로그나 홈페이지에 퍼가실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^\^